package yaml

import (
	"fmt"
	"io/ioutil"
	"os"
	"reflect"
	"strings"

	"gitee.com/xungen/goweb/errno"
	"gitee.com/xungen/goweb/utils"
)

const (
	NONE   = 0
	VALUE  = 1
	ARRAY  = 2
	OBJECT = 3
)

type YAMLNode struct {
	flag     int
	name     string
	value    string
	children []YAMLNode
}

const indentstep = "  "

func errorLine(index int) {
	panic(fmt.Sprintf("parse yaml error at line %d", index+1))
}

func trim(str string) string {
	return strings.Trim(str, " \t\r\n")
}

func spaceLength(line string) int {
	i := 0
	num := len(line)

	for i < num && line[i] == ' ' {
		i++
	}

	if i < num && line[i] == '#' {
		return num
	}

	return i
}

func spaceUnit(content []string) string {
	for _, v := range content {
		if num := spaceLength(v); num > 0 {
			return v[:num]
		}
	}

	return indentstep
}

func parse(node *YAMLNode, content []string, indent string, step string, line int) int {
	res := 0
	pos := 0
	space := indent
	linecount := len(content)
	isValue := func(msg string, pos *int) bool {
		begin := 0
		num := len(msg)
		for begin < num {
			if *pos = strings.Index(msg[begin:], ":"); *pos <= 0 {
				return true
			}
			if *pos += begin; *pos+1 >= num || msg[*pos+1] == ' ' {
				return false
			}
			begin = *pos + 1
		}
		return true
	}

	for i := line; i < linecount; i++ {
		msg := content[i]
		num := spaceLength(msg)
		if num >= len(msg) {
			continue
		}

		if mod := num % len(step); mod > 0 {
			errorLine(i)
		}

		if num > len(space) {
			errorLine(i)
		}

		if num < len(space) {
			return i - 1
		}

		msg = msg[num:]
		pos = strings.Index(msg, " #")
		if pos > 0 {
			msg = msg[:pos]
		}

		if node.name == "" {
			if isValue(msg, &pos) {
				errorLine(i)
			}

			node.flag = VALUE
			node.name = msg[:pos]
			node.value = trim(msg[pos+1:])
			if len(node.value) > 0 {
				return i
			}

			if step == "" {
				return linecount
			}

			space = indent + step
		} else {
			if msg[0] == '-' {
				if node.flag == OBJECT {
					return errno.DATAERR.Code()
				}

				item := YAMLNode{}
				if len(msg) > 1 && isValue(msg, &pos) {
					node.flag = ARRAY
					item.flag = VALUE
					item.value = trim(msg[1:])
					node.children = append(node.children, item)
				} else {
					msg = trim(msg[1:])
					backup := make([]string, 0, linecount)
					if msg == "" {
						backup = append(backup, content[i])
					} else {
						if i <= 0 {
							return errno.DATAERR.Code()
						}

						backup = append(backup, content[i])
						content[i] = space + step + msg
						i--
						backup = append(backup, content[i])
					}

					content[i] = space + "@:"
					res = parse(&item, content, space, step, i)
					if len(backup) == 1 {
						content[i], backup[0] = backup[0], content[i]
					} else {
						content[i+1], backup[0] = backup[0], content[i+1]
						content[i+0], backup[1] = backup[1], content[i+0]
					}

					if res >= i {
						i = res
						item.name = ""
						node.flag = ARRAY
						node.children = append(node.children, item)
					}
				}
			} else {
				if node.flag == ARRAY {
					return errno.DATAERR.Code()
				}

				if isValue(msg, &pos) {
					node.value = trim(msg)
					return i
				}

				item := YAMLNode{}
				if res = parse(&item, content, space, step, i); res >= i {
					node.children = append(node.children, item)
					node.flag = OBJECT
					i = res
				}
			}
		}
	}

	return len(content)
}

func (node YAMLNode) Name() string {
	return node.name
}

func (node YAMLNode) Value() string {
	return node.value
}

func (node YAMLNode) IsValue() bool {
	return node.flag == VALUE
}

func (node YAMLNode) IsArray() bool {
	return node.flag == ARRAY
}

func (node YAMLNode) IsObject() bool {
	return node.flag == OBJECT
}

func (node YAMLNode) Children() []YAMLNode {
	return node.children
}

func (node *YAMLNode) Load(path string) bool {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	buff, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}

	return node.Parse(string(buff))
}

func (node *YAMLNode) Parse(content string) bool {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	if content == "" {
		return false
	}

	lines := strings.Split(content, "\n")
	for i, v := range lines {
		v = strings.ReplaceAll(v, "\t", indentstep)
		v = strings.Trim(v, "\r")
		lines[i] = v
	}

	step := spaceUnit(lines)
	num := len(lines)
	for i := 0; i < num; i++ {
		msg := lines[i]
		if msg == "" {
			continue
		}

		if spaceLength(msg) == 0 {
			item := YAMLNode{}
			res := parse(&item, lines, "", step, i)
			if res < 0 {
				errorLine(i)
			}

			node.children = append(node.children, item)
			if res--; i < res {
				i = res
			}
		}
	}

	return len(node.children) > 0
}

func (node *YAMLNode) SetValue(data any) {
	node.flag = VALUE
	node.value = utils.ToString(data)
	node.children = node.children[0:0]
}

func (node *YAMLNode) Get(path string) *YAMLNode {
	cur := node
	arr := strings.Split(path, ".")
	for _, v := range arr {
		if cur = cur.Child(v); cur == nil {
			return nil
		}
	}
	return cur
}

func (node *YAMLNode) Child(name string) *YAMLNode {
	num := len(node.children)
	for i := 0; i < num; i++ {
		if name == node.children[i].name {
			return &node.children[i]
		}
	}
	return nil
}

func (node *YAMLNode) Create(path string) *YAMLNode {
	cur := node
	arr := strings.Split(path, ".")
	for _, v := range arr {
		item := cur.Child(v)
		if item == nil {
			cur.children = append(cur.children, YAMLNode{})
			item = &cur.children[len(cur.children)-1]
			item.name = v
			cur = item
		} else {
			cur = item
		}
	}
	return cur
}

func (node YAMLNode) String(indent ...string) string {
	res := ""
	blank := ""
	space := ""

	if len(indent) > 0 {
		space = indent[0]
	}

	if len(indent) > 1 {
		blank = indent[1]
	} else {
		blank = indentstep
	}

	if node.name == "" {
		if len(space) > 0 {
			if len(node.value) > 0 {
				return space + "- " + node.value
			}

			res = space + "-"
			space += blank
		}
	} else {
		res = space + node.name + ":"

		if len(node.value) > 0 {
			return res + " " + node.value
		}

		if len(node.children) == 0 {
			return res
		}

		space += blank
	}

	if len(node.children) > 0 {
		for _, v := range node.children {
			res += "\n" + v.String(space, blank)
		}
	}

	if len(res) > 0 && res[0] == '\n' {
		return res[1:]
	} else {
		return res
	}
}

func (node YAMLNode) Object(data any, translate bool) {
	dest := reflect.ValueOf(data).Elem()
	switch dest.Kind() {
	case reflect.Slice:
		vec := dest
		for _, v := range node.children {
			item := reflect.New(dest.Type().Elem())
			v.Object(item.Interface(), translate)
			vec = reflect.Append(vec, item.Elem())
		}
		dest.Set(vec)
	case reflect.Struct:
		none := reflect.Value{}
		for _, v := range node.children {
			field := dest.FieldByName(v.name)
			if field == none {
				v.name = utils.Capitalize(v.name)
				field = dest.FieldByName(utils.Capitalize(v.name))
				if field == none {
					continue
				}
			}
			v.Object(field.Addr().Interface(), translate)
		}
	default:
		if translate && len(node.value) > 1 {
			typ := dest.Type().Kind()
			msg := os.ExpandEnv(node.value)
			if utils.IsInteger(typ) {
				pos := len(msg) - 1
				msg := strings.ToUpper(msg)
				if strings.HasSuffix(msg, "B") {
					msg = msg[:pos]
					pos--
				}
				if strings.HasSuffix(msg, "K") {
					msg = msg[:pos]
					pos = 1024
				} else if strings.HasSuffix(msg, "M") {
					msg = msg[:pos]
					pos = 1024 * 1024
				} else if strings.HasSuffix(msg, "G") {
					msg = msg[:pos]
					pos = 1024 * 1024 * 1024
				} else {
					pos = 1
				}
				switch typ {
				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
					dest.SetInt(int64(pos) * errno.IgnoreError(utils.ToInteger[int64](msg)))
				case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
					dest.SetUint(uint64(pos) * errno.IgnoreError(utils.ToInteger[uint64](msg)))	
				}
			} else {
				utils.SetFieldValue(dest, msg)
			}
		} else {
			utils.SetFieldValue(dest, node.value)
		}
	}
}

func Unmarshal(content []byte, dest any, translate bool) bool {
	node := YAMLNode{}
	if node.Parse(string(content)) {
		node.Object(dest, translate)
		return true
	}
	return false
}
